]> git.saurik.com Git - apple/security.git/blob - Keychain Circle Notification/KNAppDelegate.m
Security-55471.14.tar.gz
[apple/security.git] / Keychain Circle Notification / KNAppDelegate.m
1 //
2 // KNAppDelegate.m
3 // Keychain Circle Notification
4 //
5 // Created by J Osborne on 2/21/13.
6 //
7 //
8
9 #import "KNAppDelegate.h"
10 #import "KDSecCircle.h"
11 #import "KDCirclePeer.h"
12 #import "NSDictionary+compactDescription.h"
13 #import <AOSUI/NSImageAdditions.h>
14 #import <AppleSystemInfo/AppleSystemInfo.h>
15 #import <Security/SecFrameworkStrings.h>
16
17 #import <AOSAccounts/MobileMePrefsCoreAEPrivate.h>
18 #import <AOSAccounts/MobileMePrefsCore.h>
19
20 static char *kLaunchLaterXPCName = "com.apple.security.Keychain-Circle-Notification-TICK";
21
22 @implementation KNAppDelegate
23
24 static NSUserNotificationCenter *appropriateNotificationCenter()
25 {
26 return [NSUserNotificationCenter _centerForIdentifier:@"com.apple.security.keychain-circle-notification" type:_NSUserNotificationCenterTypeSystem];
27 }
28
29 -(void)notifyiCloudPreferencesAbout:(NSString *)eventName;
30 {
31 if (nil == eventName) {
32 return;
33 }
34
35 NSString *account = (__bridge NSString *)(MMCopyLoggedInAccount());
36 NSLog(@"notifyiCloudPreferencesAbout %@", eventName);
37
38 AEDesc aeDesc;
39 BOOL createdAEDesc = createAEDescWithAEActionAndAccountID((__bridge NSString *)kMMServiceIDKeychainSync, eventName, account, &aeDesc);
40 if (createdAEDesc)
41 {
42 OSErr err;
43 LSLaunchURLSpec lsSpec;
44
45 lsSpec.appURL = NULL;
46 lsSpec.itemURLs = (__bridge CFArrayRef)([NSArray arrayWithObject:[NSURL fileURLWithPath:@"/System/Library/PreferencePanes/iCloudPref.prefPane"]]);
47 lsSpec.passThruParams = &aeDesc;
48 lsSpec.launchFlags = kLSLaunchDefaults | kLSLaunchAsync;
49 lsSpec.asyncRefCon = NULL;
50
51 err = LSOpenFromURLSpec(&lsSpec, NULL);
52
53 if (err) {
54 NSLog(@"Can't send event %@, err=%d", eventName, err);
55 }
56 AEDisposeDesc(&aeDesc);
57 }
58 else
59 {
60 NSLog(@"unable to create and send aedesc for account: '%@' and action: '%@'\n", account, eventName);
61 }
62 }
63
64 -(void)showiCloudPrefrences
65 {
66 static NSAppleScript *script = nil;
67 if (!script) {
68 script = [[NSAppleScript alloc] initWithSource:@"tell application \"System Preferences\"\n\
69 activate\n\
70 set the current pane to pane id \"com.apple.preferences.icloud\"\n\
71 end tell"];
72 }
73
74 NSDictionary *appleScriptError = nil;
75 [script executeAndReturnError:&appleScriptError];
76
77 if (appleScriptError) {
78 NSLog(@"appleScriptError: %@", appleScriptError);
79 } else {
80 NSLog(@"NO appleScript error");
81 }
82 }
83
84 -(void)timerCheck
85 {
86 NSDate *nowish = [NSDate new];
87 self.state = [KNPersistantState loadFromStorage];
88 if ([nowish compare:self.state.pendingApplicationReminder] != NSOrderedAscending) {
89 NSLog(@"REMINDER TIME: %@ >>> %@", nowish, self.state.pendingApplicationReminder);
90 // self.circle.rawStatus might not be valid yet
91 if (SOSCCThisDeviceIsInCircle(NULL) == kSOSCCRequestPending) {
92 // Still have a request pending, send reminder, and also in addtion to the UI
93 // we need to send a notification for iCloud pref pane to pick up
94
95 CFNotificationCenterPostNotificationWithOptions(CFNotificationCenterGetDistributedCenter(), CFSTR("com.apple.security.secureobjectsync.pendingApplicationReminder"), (__bridge const void *)([self.state.applcationDate description]), NULL, 0);
96
97 [self postApplicationReminder];
98 self.state.pendingApplicationReminder = [self.state.applcationDate dateByAddingTimeInterval:[self getPendingApplicationReminderInterval]];
99 [self.state writeToStorage];
100 }
101 }
102 }
103
104 -(void)scheduleActivityAt:(NSDate*)time
105 {
106 if ([time compare:[NSDate distantFuture]] != NSOrderedSame) {
107 NSTimeInterval howSoon = [time timeIntervalSinceNow];
108 if (howSoon > 0) {
109 [self scheduleActivityIn:howSoon];
110 } else {
111 [self timerCheck];
112 }
113 }
114 }
115
116 -(void)scheduleActivityIn:(int)alertInterval
117 {
118 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
119 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
120 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
121 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
122 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
123 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
124
125 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
126 [self timerCheck];
127 });
128 }
129
130 -(NSTimeInterval)getPendingApplicationReminderInterval
131 {
132 if (self.state.pendingApplicationReminderInterval) {
133 return [self.state.pendingApplicationReminderInterval doubleValue];
134 } else {
135 return 48*24*60*60;
136 }
137 }
138
139 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
140 {
141 appropriateNotificationCenter().delegate = self;
142
143 NSLog(@"Posted at launch: %@", appropriateNotificationCenter().deliveredNotifications);
144
145 self.viewedIds = [NSMutableSet new];
146 self.circle = [KDSecCircle new];
147 self.state = [KNPersistantState loadFromStorage];
148 KNAppDelegate *me = self;
149
150 [self.circle addChangeCallback:^{
151 me.state = [KNPersistantState loadFromStorage];
152 if ((me.state.lastCircleStatus == kSOSCCInCircle && !me.circle.isInCircle) || me.state.debugLeftReason) {
153 enum DepartureReason reason = kSOSNeverLeftCircle;
154 if (me.state.debugLeftReason) {
155 reason = [me.state.debugLeftReason intValue];
156 me.state.debugLeftReason = nil;
157 } else {
158 CFErrorRef err = NULL;
159 reason = SOSCCGetLastDepartureReason(&err);
160 if (reason == kSOSDepartureReasonError) {
161 NSLog(@"SOSCCGetLastDepartureReason err: %@", err);
162 }
163 }
164
165 //NSString *model = (__bridge NSString *)(ASI_CopyComputerModelName(FALSE));
166 NSString *body = nil;
167 switch (reason) {
168 case kSOSDepartureReasonError:
169 case kSOSNeverLeftCircle:
170 case kSOSWithdrewMembership:
171 break;
172
173 default:
174 NSLog(@"Unknown departure reason %d", reason);
175 // fallthrough on purpose
176
177 case kSOSMembershipRevoked:
178 case kSOSLeftUntrustedCircle:
179 body = NSLocalizedString(@"Approve this Mac from another device to use iCloud Keychain.", @"Body for iCloud Keychain Reset notification");
180 break;
181 }
182 [me.state writeToStorage];
183 NSLog(@"departure reason %d, body=%@", reason, body);
184 if (body) {
185 [me postKickedOutWithMessage: body];
186 }
187 }
188
189 [me timerCheck];
190
191 if (me.state.lastCircleStatus != kSOSCCRequestPending && me.circle.rawStatus == kSOSCCRequestPending) {
192 NSLog(@"Entered RequestPending");
193 NSDate *nowish = [NSDate new];
194 me.state.applcationDate = nowish;
195 me.state.pendingApplicationReminder = [me.state.applcationDate dateByAddingTimeInterval:[me getPendingApplicationReminderInterval]];
196 [me.state writeToStorage];
197 [me scheduleActivityAt:me.state.pendingApplicationReminder];
198 }
199
200 NSMutableSet *applicantIds = [NSMutableSet new];
201 for (KDCirclePeer *applicant in me.circle.applicants) {
202 if (!me.circle.isInCircle) {
203 // We don't want to yammer on about circles we aren't in,
204 // and we don't want to be extra confusing announcing our
205 // own join requests as if the user could approve them
206 // locally!
207 break;
208 }
209 [me postForApplicant:applicant];
210 [applicantIds addObject:applicant.idString];
211 }
212
213 NSUserNotificationCenter *notificationCenter = appropriateNotificationCenter();
214 NSLog(@"Checking validity of %lu notes", (unsigned long)notificationCenter.deliveredNotifications.count);
215 for (NSUserNotification *note in notificationCenter.deliveredNotifications) {
216 if (note.userInfo[@"applicantId"] && ![applicantIds containsObject:note.userInfo[@"applicantId"]]) {
217 NSLog(@"No longer an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]);
218 [notificationCenter removeDeliveredNotification:note];
219 } else {
220 NSLog(@"Still an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]);
221 }
222 }
223
224 me.state.lastCircleStatus = me.circle.rawStatus;
225
226 [me.state writeToStorage];
227 }];
228
229 [me scheduleActivityAt:me.state.pendingApplicationReminder];
230 }
231
232 -(BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
233 {
234 return YES;
235 }
236
237 -(void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
238 {
239 if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
240 [self notifyiCloudPreferencesAbout:notification.userInfo[@"Activate"]];
241 }
242
243 // The "Later" seems handled Ok without doing anything here, but KickedOut & other special items need an action
244 if (notification.userInfo[@"SPECIAL"]) {
245 NSLog(@"ACTIVATED (remove): %@", notification);
246 [appropriateNotificationCenter() removeDeliveredNotification:notification];
247 } else {
248 NSLog(@"ACTIVATED (NOT removed): %@", notification);
249 }
250 }
251
252 -(void)userNotificationCenter:(NSUserNotificationCenter *)center didDismissAlert:(NSUserNotification *)notification
253 {
254 [self notifyiCloudPreferencesAbout:notification.userInfo[@"Dismiss"]];
255
256 if (!notification.userInfo[@"SPECIAL"]) {
257 // If we don't do anything here & another notification comes in we
258 // will repost the alert, which will be dumb.
259 id applicantId = notification.userInfo[@"applicantId"];
260 if (applicantId != nil) {
261 [self.viewedIds addObject:applicantId];
262 }
263 NSLog(@"DISMISS (t) %@", notification);
264 } else {
265 NSLog(@"DISMISS (f) %@", notification);
266 [appropriateNotificationCenter() removeDeliveredNotification:notification];
267 }
268 }
269
270 -(void)postForApplicant:(KDCirclePeer*)applicant
271 {
272 static int postCount = 0;
273
274 if ([self.viewedIds containsObject:applicant.idString]) {
275 NSLog(@"Already viewed %@, skipping", applicant);
276 return;
277 }
278
279 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
280 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
281 if ([applicant.idString isEqualToString:note.userInfo[@"applicantId"]]) {
282 if (note.isPresented) {
283 NSLog(@"Already posted&presented: %@ (I=%@)", note, note.userInfo);
284 return;
285 } else {
286 NSLog(@"Already posted, but not presented: %@ (I=%@)", note, note.userInfo);
287 }
288 }
289 }
290
291 NSUserNotification *note = [NSUserNotification new];
292
293 // Genstrings command line is: genstrings -o en.lproj -u KNAppDelegate.m
294 note.title = [NSString stringWithFormat:NSLocalizedString(@"iCloud Keychain", @"Title for new keychain syncing device notification")];
295 note.informativeText = [NSString stringWithFormat:NSLocalizedString(@"\\U201C%1$@\\U201D wants to use your passwords.", @"Message text for new keychain syncing device notification"), applicant.name];
296
297 note.hasActionButton = YES;
298 note._displayStyle = _NSUserNotificationDisplayStyleAlert;
299 note._identityImage = [NSImage bundleImage];
300 note._identityImageHasBorder = NO;
301 note._actionButtonIsSnooze = YES;
302 note.actionButtonTitle = NSLocalizedString(@"Later", @"Button label to dismiss device notification");
303 note.otherButtonTitle = NSLocalizedString(@"View", @"Button label to view device notification");
304
305 note.identifier = [[NSUUID new] UUIDString];
306
307 note.userInfo = @{@"applicantName": applicant.name,
308 @"applicantId": applicant.idString,
309 @"Dismiss": (__bridge NSString *)kMMPropertyKeychainAADetailsAEAction,
310 };
311
312 NSLog(@"About to post#%d/%lu (%@): %@", postCount, (unsigned long)noteCenter.deliveredNotifications.count, applicant.idString, note);
313 [appropriateNotificationCenter() deliverNotification:note];
314
315 postCount++;
316 }
317
318 -(void)postKickedOutWithMessage:(NSString*)body
319 {
320 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
321 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
322 if (note.userInfo[@"KickedOut"]) {
323 if (note.isPresented) {
324 NSLog(@"Already posted&presented (removing): %@", note);
325 [appropriateNotificationCenter() removeDeliveredNotification: note];
326 } else {
327 NSLog(@"Already posted, but not presented: %@", note);
328 }
329 }
330 }
331
332 NSUserNotification *note = [NSUserNotification new];
333
334 note.title = NSLocalizedString(@"iCloud Keychain Was Reset", @"Title for iCloud Keychain Reset notification");
335 note.informativeText = body; // Already LOCed
336
337 note._identityImage = [NSImage bundleImage];
338 note._identityImageHasBorder = NO;
339 note.otherButtonTitle = NSLocalizedString(@"Close", @"Close button");
340 note.actionButtonTitle = NSLocalizedString(@"Options", @"Options Button");
341
342 note.identifier = [[NSUUID new] UUIDString];
343
344 note.userInfo = @{@"KickedOut": @1,
345 @"SPECIAL": @1,
346 @"Activate": (__bridge NSString *)kMMPropertyKeychainMRDetailsAEAction,
347 };
348
349 NSLog(@"About to post#-/%lu (KICKOUT): %@", (unsigned long)noteCenter.deliveredNotifications.count, note);
350 [appropriateNotificationCenter() deliverNotification:note];
351 }
352
353 -(void)postApplicationReminder
354 {
355 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
356 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
357 if (note.userInfo[@"ApplicationReminder"]) {
358 if (note.isPresented) {
359 NSLog(@"Already posted&presented (removing): %@", note);
360 [appropriateNotificationCenter() removeDeliveredNotification: note];
361 } else {
362 NSLog(@"Already posted, but not presented: %@", note);
363 }
364 }
365 }
366
367 NSUserNotification *note = [NSUserNotification new];
368
369 note.title = NSLocalizedString(@"iCloud Keychain", @"Title for iCloud Keychain Application still pending (from this device) reminder");
370 note.informativeText = NSLocalizedString(@"Approve this Mac from another device to use iCloud Keychain.", @"Body text for iCloud Keychain Application still pending (from this device) reminder");
371
372 note._identityImage = [NSImage bundleImage];
373 note._identityImageHasBorder = NO;
374 note.otherButtonTitle = NSLocalizedString(@"Close", @"Close button");
375 note.actionButtonTitle = NSLocalizedString(@"Options", @"Options Button");
376
377 note.identifier = [[NSUUID new] UUIDString];
378
379 note.userInfo = @{@"ApplicationReminder": @1,
380 @"SPECIAL": @1,
381 @"Activate": (__bridge NSString *)kMMPropertyKeychainWADetailsAEAction,
382 };
383
384 NSLog(@"About to post#-/%lu (REMINDER): %@", (unsigned long)noteCenter.deliveredNotifications.count, note);
385 [appropriateNotificationCenter() deliverNotification:note];
386 }
387
388 @end